/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.feilong.lib.javassist.bytecode.annotation;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.feilong.lib.javassist.ClassPool;
import com.feilong.lib.javassist.CtClass;
import com.feilong.lib.javassist.NotFoundException;
import com.feilong.lib.javassist.bytecode.AnnotationDefaultAttribute;
import com.feilong.lib.javassist.bytecode.ClassFile;
import com.feilong.lib.javassist.bytecode.MethodInfo;

/**
 * Internal-use only. This is a helper class internally used for implementing
 * <code>toAnnotationType()</code> in <code>Annotation</code>.
 * 
 * @author Shigeru Chiba
 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
 * @author <a href="mailto:adrian@jboss.org">Adrian Brock</a>
 */
public class AnnotationImpl implements InvocationHandler{

    private static final String JDK_ANNOTATION_CLASS_NAME  = "java.lang.annotation.Annotation";

    private static Method       JDK_ANNOTATION_TYPE_METHOD = null;

    private Annotation          annotation;

    private ClassPool           pool;

    private ClassLoader         classLoader;

    private transient Class<?>  annotationType;

    private transient int       cachedHashCode             = Integer.MIN_VALUE;

    static{
        // Try to resolve the JDK annotation type method
        try{
            Class<?> clazz = Class.forName(JDK_ANNOTATION_CLASS_NAME);
            JDK_ANNOTATION_TYPE_METHOD = clazz.getMethod("annotationType", (Class[]) null);
        }catch (Exception ignored){
            // Probably not JDK5+
        }
    }

    /**
     * Constructs an annotation object.
     *
     * @param cl
     *            class loader for obtaining annotation types.
     * @param clazz
     *            the annotation type.
     * @param cp
     *            class pool for containing an annotation
     *            type (or null).
     * @param anon
     *            the annotation.
     * @return the annotation
     */
    public static Object make(ClassLoader cl,Class<?> clazz,ClassPool cp,Annotation anon) throws IllegalArgumentException{
        AnnotationImpl handler = new AnnotationImpl(anon, cp, cl);
        return Proxy.newProxyInstance(cl, new Class[] { clazz }, handler);
    }

    private AnnotationImpl(Annotation a, ClassPool cp, ClassLoader loader){
        annotation = a;
        pool = cp;
        classLoader = loader;
    }

    /**
     * Obtains the name of the annotation type.
     * 
     * @return the type name
     */
    public String getTypeName(){
        return annotation.getTypeName();
    }

    /**
     * Get the annotation type
     * 
     * @return the annotation class
     * @throws NoClassDefFoundError
     *             when the class could not loaded
     */
    private Class<?> getAnnotationType(){
        if (annotationType == null){
            String typeName = annotation.getTypeName();
            try{
                annotationType = classLoader.loadClass(typeName);
            }catch (ClassNotFoundException e){
                NoClassDefFoundError error = new NoClassDefFoundError("Error loading annotation class: " + typeName);
                error.setStackTrace(e.getStackTrace());
                throw error;
            }
        }
        return annotationType;
    }

    /**
     * Obtains the internal data structure representing the annotation.
     * 
     * @return the annotation
     */
    public Annotation getAnnotation(){
        return annotation;
    }

    /**
     * Executes a method invocation on a proxy instance.
     * The implementations of <code>toString()</code>, <code>equals()</code>,
     * and <code>hashCode()</code> are directly supplied by the
     * <code>AnnotationImpl</code>. The <code>annotationType()</code> method
     * is also available on the proxy instance.
     */
    @Override
    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
        String name = method.getName();
        if (Object.class == method.getDeclaringClass()){
            if ("equals".equals(name)){
                Object obj = args[0];
                return Boolean.valueOf(checkEquals(obj));
            }else if ("toString".equals(name)){
                return annotation.toString();
            }else if ("hashCode".equals(name)){
                return Integer.valueOf(hashCode());
            }
        }else if ("annotationType".equals(name) && method.getParameterTypes().length == 0){
            return getAnnotationType();
        }

        MemberValue mv = annotation.getMemberValue(name);
        if (mv == null){
            return getDefault(name, method);
        }
        return mv.getValue(classLoader, pool, method);
    }

    private Object getDefault(String name,Method method) throws ClassNotFoundException,RuntimeException{
        String classname = annotation.getTypeName();
        if (pool != null){
            try{
                CtClass cc = pool.get(classname);
                ClassFile cf = cc.getClassFile2();
                MethodInfo minfo = cf.getMethod(name);
                if (minfo != null){
                    AnnotationDefaultAttribute ainfo = (AnnotationDefaultAttribute) minfo.getAttribute(AnnotationDefaultAttribute.tag);
                    if (ainfo != null){
                        MemberValue mv = ainfo.getDefaultValue();
                        return mv.getValue(classLoader, pool, method);
                    }
                }
            }catch (NotFoundException e){
                throw new RuntimeException("cannot find a class file: " + classname);
            }
        }

        throw new RuntimeException("no default value: " + classname + "." + name + "()");
    }

    /**
     * Returns a hash code value for this object.
     */
    @Override
    public int hashCode(){
        if (cachedHashCode == Integer.MIN_VALUE){
            int hashCode = 0;

            // Load the annotation class
            getAnnotationType();

            Method[] methods = annotationType.getDeclaredMethods();
            for (Method method : methods){
                String name = method.getName();
                int valueHashCode = 0;

                // Get the value
                MemberValue mv = annotation.getMemberValue(name);
                Object value = null;
                try{
                    if (mv != null){
                        value = mv.getValue(classLoader, pool, method);
                    }
                    if (value == null){
                        value = getDefault(name, method);
                    }
                }catch (RuntimeException e){
                    throw e;
                }catch (Exception e){
                    throw new RuntimeException("Error retrieving value " + name + " for annotation " + annotation.getTypeName(), e);
                }

                // Calculate the hash code
                if (value != null){
                    if (value.getClass().isArray()){
                        valueHashCode = arrayHashCode(value);
                    }else{
                        valueHashCode = value.hashCode();
                    }
                }
                hashCode += 127 * name.hashCode() ^ valueHashCode;
            }

            cachedHashCode = hashCode;
        }
        return cachedHashCode;
    }

    /**
     * Check that another annotation equals ourselves.
     * 
     * @param obj
     *            the other annotation
     * @return the true when equals false otherwise
     * @throws Exception
     *             for any problem
     */
    private boolean checkEquals(Object obj) throws Exception{
        if (obj == null){
            return false;
        }

        // Optimization when the other is one of ourselves
        if (obj instanceof Proxy){
            InvocationHandler ih = Proxy.getInvocationHandler(obj);
            if (ih instanceof AnnotationImpl){
                AnnotationImpl other = (AnnotationImpl) ih;
                return annotation.equals(other.annotation);
            }
        }

        Class<?> otherAnnotationType = (Class<?>) JDK_ANNOTATION_TYPE_METHOD.invoke(obj);
        if (getAnnotationType().equals(otherAnnotationType) == false){
            return false;
        }

        Method[] methods = annotationType.getDeclaredMethods();
        for (Method method : methods){
            String name = method.getName();

            // Get the value
            MemberValue mv = annotation.getMemberValue(name);
            Object value = null;
            Object otherValue = null;
            try{
                if (mv != null){
                    value = mv.getValue(classLoader, pool, method);
                }
                if (value == null){
                    value = getDefault(name, method);
                }
                otherValue = method.invoke(obj);
            }catch (RuntimeException e){
                throw e;
            }catch (Exception e){
                throw new RuntimeException("Error retrieving value " + name + " for annotation " + annotation.getTypeName(), e);
            }

            if (value == null && otherValue != null){
                return false;
            }
            if (value != null && value.equals(otherValue) == false){
                return false;
            }
        }

        return true;
    }

    /**
     * Calculates the hashCode of an array using the same
     * algorithm as java.util.Arrays.hashCode()
     * 
     * @param object
     *            the object
     * @return the hashCode
     */
    private static int arrayHashCode(Object object){
        if (object == null){
            return 0;
        }

        int result = 1;

        Object[] array = (Object[]) object;
        for (Object element : array){
            int elementHashCode = 0;
            if (element != null){
                elementHashCode = element.hashCode();
            }
            result = 31 * result + elementHashCode;
        }
        return result;
    }
}
